Skip to content

generated code: keep source.py parseable/runnable on a pre-3.13 target (PyPy 3.11) - #256

Merged
devdanzin merged 1 commit into
mainfrom
pypy311-fstring-compat
Aug 18, 2026
Merged

generated code: keep source.py parseable/runnable on a pre-3.13 target (PyPy 3.11)#256
devdanzin merged 1 commit into
mainfrom
pypy311-fstring-compat

Conversation

@devdanzin

Copy link
Copy Markdown
Owner

Why

fusil's requires-python floor of 3.13 constrains the runner. The generated
source.py is executed by --python, which is routinely older or alternative — PyPy 3.11
is a fuzzing target in its own right.

Two 3.12+/3.13+ constructs leaked into the emitted script and aborted every session
before any fuzzing happened
:

  1. f"{expr!r }" — a space between the conversion and the closing brace is PEP 701
    syntax (3.12+). On a 3.11 target the entire script is a SyntaxError, so nothing
    parses. Three emitter sites in write_python_code.py; tightening them to {expr!r}
    leaves the printed text byte-identical.
  2. types.CapsuleType — 3.13+, read unguarded in the tricky-objects prelude. On PyPy
    3.11 every session died at that line with AttributeError. Now guarded exactly the way
    the neighbouring types.GenericAlias already is; the if tricky_capsule: use site
    immediately below already expects a falsy value.

Why this is worth a test

Both failure modes are invisible from the outside: the run looks clean — no crashes,
no kept dirs — because nothing ever ran. A 12-session PyPy run reported zero findings while
in fact zero calls had been fuzzed.

Measured on PyPy 3.11.15 / 7.3.23 (--discover-in-target, CPython 3.14 runner): sessions
went from dying in the prelude to thousands of lines of real activity each.

Tests

tests/python/test_target_py311_compat.py — verified each test fails when its fix is
reverted:

  • execs the tricky-objects prelude under a types shim with CapsuleType removed, asserting
    it survives and yields None;
  • scans every emitter under fusil/ for a conversion field followed by whitespace, and
    checks the golden snapshot for the same.

Full suite green (1247 tests); ruff check + ruff format --check clean. Golden snapshot
regenerated for the intentional output change.

🤖 Generated with Claude Code

fusil's own floor is 3.13, but that constrains the *runner*. The generated script is
executed by --python, which is routinely older -- PyPy 3.11 is a fuzzing target in its
own right. Two constructs leaked into the emitted script and killed every session
before any fuzzing happened:

- `f"{expr!r }"` (a space between the conversion and the closing brace) is PEP 701
  syntax, so the whole script is a SyntaxError on a 3.11 target. Three emitter sites in
  write_python_code.py; tightening them to `{expr!r}` leaves the printed text identical.
- `types.CapsuleType` is 3.13+, read unguarded in the tricky-objects prelude. On PyPy
  3.11 every session died at that line with AttributeError. Guarded the same way the
  neighbouring `types.GenericAlias` already is; the `if tricky_capsule:` use site below
  it already expects a falsy value.

Both failure modes are invisible from the outside: the run looks *clean* (no crashes
kept) because nothing ever ran. Measured on PyPy 3.11.15 / 7.3.23, a 12-session run went
from 0 fuzzed calls to thousands of lines of real activity per session.

tests/python/test_target_py311_compat.py locks both in (each fails if its fix is
reverted). Golden snapshot regenerated for the intentional output change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhcpLoyjUWLbETGZnA9boj
@devdanzin
devdanzin merged commit 6a54d58 into main Aug 18, 2026
1 of 2 checks passed
@devdanzin
devdanzin deleted the pypy311-fstring-compat branch August 18, 2026 20:55
Nortaq-PlayNexus pushed a commit to Nortaq-PlayNexus/fusil that referenced this pull request Sep 4, 2026
`gc.set_threshold` is CPython-only. The prelude runs on the TARGET, so on PyPy the
unguarded call raised AttributeError at MODULE LEVEL, at line 55, before a single
target call:

    import gc
    gc.set_threshold(1, 1, 1)
    AttributeError: module 'gc' has no attribute 'set_threshold'

The failure mode is the bad part: the sessions do not look broken, they look CLEAN.
A 43 368-session PyPy fleet ran to completion reporting zero crashes, zero timeouts,
zero cpu-load kills and 0.3-second sessions, and nothing in any log said why. Every
one of those sessions was dead on arrival.

Verified on a real script out of that fleet: before the guard it produces 6 lines of
output and exits 1; after, 500 lines and it is fuzzing (`--- Fuzzing 40 functions in
traceback ---`).

The flag stays a no-op rather than an error on such a target -- it is a tuning knob,
not a request, and PyPy's GC is not generational in CPython's sense so there is
nothing to coerce.

Two tests: one asserts the call is inside a try/except AttributeError by walking the
AST rather than matching text, and one execs the emitted snippet against a gc module
with no set_threshold, which is what PyPy looks like from the prelude's point of view.

Same shape as the pre-3.13 target-compat break fixed in devdanzin#256. The general rule that
keeps being relearned: anything CPython-specific in the prelude must be guarded,
because the prelude runs on the target.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant